Terraformでmappingをmoduleに渡す方法
はじめに
こんにちは、中山です。
みなさんはTerraformのmappingやmoduleを利用していますか。mappingについては弊社八幡のTerraformで複数台のEC2インスタンスを構築する場合のTIPSに詳しいです。簡単に説明すると、mappingとはいわゆるhashのように変数を管理するデータ構造です。moduleとは要するに関数ですね。Terraformで利用するresourceをひとまとめにして再利用可能な形にパッケージ化できます。Terraformにはこのようにプログラミング言語と同等の機能があるので表現力がとても高く、小さなプログラミング言語のような側面があります。
私はこれらの機能をよく利用しているのですが、ふとこう思ったことがあります。「mappingで定義した変数をmoduleに渡せたら便利だな」と。私はTerraformを利用するとき main.tf
に全てのresourceを書くのではなくコンポーネント毎(NW/EC2/RDSなど)にmoduleでtfファイルを分割しています。そしてそれらを束ねる大本のtfファイルから terraform.tfvars
を参照するという方式でコードを書くことが多いです。
こうすることによって以下のメリットがあると考えています。
- 全ての変数を
terraform.tfvars
で管理するので変数の管理が容易 - コンポーネント毎にコードを分割することでコードの見通しが良くなる
参考にしているのはhashicorp/best-practicesというリポジトリです。Terraformの開発元、HashiCorpがbestと言っているのだからそれなりの理由があるんだろうという訳で今のところこの方式でコードを書いています(これはこれできついものがあるのですがそれはまた別エントリにまとめます)。
話を戻します。Terraformはmoduleに変数を渡す際、変数を渡すtfファイル/変数を受け取るmodule両方に変数を定義する必要があります。変数の数が増えるとこの変数定義が結構面倒くさいです。そこでmappingとして変数をまとめれば便利なのではと思った次第です。
本エントリではこれが実現できるのか、その試行錯誤をメインにしてご紹介します。
結論
先に結論を書きます。v0.7.0対応するようです!!!111。つまりそれ以下のバージョンでは対応してません。なので下の文章読まなくてもいいです。もしお時間ありましたらお付き合いください。
出力結果の確認方法
Terraformの output
サブコマンドは terraform.tfstate
から入力を得ているため、適宜 plan
サブコマンドでそのファイルを作成する必要があります。本エントリのサンプルコードは出力結果を確認する前に plan
サブコマンドで terraform.tfstate
ファイルを作成してください。
Terraformのバージョン
v0.6.16です。
テスト1: *.tf
ファイルでmappingを定義してそのまま出力してみる
まずは通常の使い方で期待する動作を確認してみましょう。 terraform.tfvars
ではなく *.tf
でmappingを定義し、moduleではなくそのまま出力してみます。
main.tf
variable "images" { default = { us-east-1 = "image-1234" us-west-2 = "image-4567" } } output "us-east-1_image_id_via_lookup" { value = "${lookup(var.images, "us-east-1")}" } output "us-east-1_image_id_via_dot" { value = "${var.images.us-east-1}" } output "us-west-2_image_id_via_lookup" { value = "${lookup(var.images, "us-west-2")}" } output "us-west-2_image_id_via_dot" { value = "${var.images.us-west-2}" }
mappingはlookup関数だけではなく、 ${var.map.key}
の形式でも参照可能です。実行してみましょう。
$ terraform output us-east-1_image_id_via_dot = image-1234 us-east-1_image_id_via_lookup = image-1234 us-west-2_image_id_via_dot = image-4567 us-west-2_image_id_via_lookup = image-4567
予想通り images
の値がそれぞれ参照できているようですね。
テスト2: terraform.tfvars
でmappingを定義してそのまま出力してみる
*.tf
ではなく terraform.tfvars
でもmappingは定義できます。こちらも期待する動作をするのか実際に試してみましょう。
- terraform.tfvars
images.us-east-1 = "image-1234" images.us-west-2 = "image-4567"
- main.tf
variable "images" { default = {} } output "us-east-1_image_id_via_lookup" { value = "${lookup(var.images, "us-east-1")}" } output "us-east-1_image_id_via_dot" { value = "${var.images.us-east-1}" } output "us-west-2_image_id_via_lookup" { value = "${lookup(var.images, "us-west-2")}" } output "us-west-2_image_id_via_dot" { value = "${var.images.us-west-2}" }
ポイントは main.tf
で定義している images
変数の default = {}
という指定です。これを指定しないとmappingとして認識されないようです。実行してみましょう。
$ terraform output us-east-1_image_id_via_dot = image-1234 us-east-1_image_id_via_lookup = image-1234 us-west-2_image_id_via_dot = image-4567 us-west-2_image_id_via_lookup = image-4567
先程と同じような出力結果が得られました。
テスト3: *.tf
で定義したmappingをmoduleに渡して出力してみる
期待する動作をテスト1/テスト2で確認しました。今度はmoduleにmappingで定義した変数を渡し、上記のような期待する動作になるのか確認してみましょう。まずは、 mappingを *.tf
で定義してみます。
main.tf
variable "images" { default = { us-east-1 = "image-1234" us-west-2 = "image-4567" } } module "test" { source = "./test" images = "${var.images}" } output "us-east-1_image_id_via_lookup" { value = "${module.test.us-east-1_image_id_via_lookup}" } output "us-east-1_image_id_via_dot" { value = "${module.test.us-east-1_image_id_via_dot}" } output "us-west-2_image_id_via_lookup" { value = "${module.test.us-west-2_image_id_via_lookup}" } output "us-west-2_image_id_via_dot" { value = "${module.test.us-west-2_image_id_via_dot}" }
test/test.tf
variable "images" { default = {} } output "us-east-1_image_id_via_lookup" { value = "${lookup(var.images, "us-east-1")}" } output "us-east-1_image_id_via_dot" { value = "${var.images.us-east-1}" } output "us-west-2_image_id_via_lookup" { value = "${lookup(var.images, "us-west-2")}" } output "us-west-2_image_id_via_dot" { value = "${var.images.us-west-2}" }
実行してみます。
$ terraform plan Error configuring: 1 error(s) occurred: * variable images in module test should be type map, got type string
どうやらmappingとして定義したはずの images
変数が文字列として解釈されてしまうようです。Terraformは変数を定義する際にそのタイプを明示的に宣言することができます。具体的には type = "map"
と指定すればmappingとして定義できます。が、こちらを指定してみても同じ結果でした。残念。
テスト4: terraform.tfvars
で定義したmappingをmoduleに渡して出力してみる
今度は terraform.tfvars
でmappingを定義してみましょう。動くのでしょうか。
terraform.tfvars
images.us-east-1 = "image-1234" images.us-west-2 = "image-4567"
main.tf
variable "images" { default = {} } module "test" { source = "./test" images = "${var.images}" } output "us-east-1_image_id_via_lookup" { value = "${module.test.us-east-1_image_id_via_lookup}" } output "us-east-1_image_id_via_dot" { value = "${module.test.us-east-1_image_id_via_dot}" } output "us-west-2_image_id_via_lookup" { value = "${module.test.us-west-2_image_id_via_lookup}" } output "us-west-2_image_id_via_dot" { value = "${module.test.us-west-2_image_id_via_dot}" }
test/test.tf
variable "images" { default = {} } output "us-east-1_image_id_via_lookup" { value = "${lookup(var.images, "us-east-1")}" } output "us-east-1_image_id_via_dot" { value = "${var.images.us-east-1}" } output "us-west-2_image_id_via_lookup" { value = "${lookup(var.images, "us-west-2")}" } output "us-west-2_image_id_via_dot" { value = "${var.images.us-west-2}" }
実行してみます。
$ terraform plan Error configuring: 1 error(s) occurred: * variable images in module test should be type map, got type string
残念ながら同じようなエラーが出てしまいますね。 type = "map"
にしても同じようです。
テスト5: keys/values関数を使う
Terraformにはドキュメントには記載されていないkeys/values関数があります。この関数を利用することで「擬似的」にmappingをmoduleに渡すことができます。
terraform.tfvars
images.us-east-1 = "image-1234" images.us-west-2 = "image-4567"
main.tf
variable "images" { default = {} } module "test" { source = "./test" image_keys = "${join(",", keys(var.images))}" image_values = "${join(",", values(var.images))}" } output "us-east-1_image_id" { value = "${module.test.us-east-1_image_id}" } output "us-west-2_image_id" { value = "${module.test.us-west-2_image_id}" }
test/test.tf
variable "image_keys" {} variable "image_values" {} output "us-east-1_image_id" { value = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-east-1"))}" } output "us-west-2_image_id" { value = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-west-2"))}" }
keys関数でmappingのkeyを取得、values関数でmappingのvalueを取得し、join関数でカンマ区切りの文字列に変換してmoduleに渡しています。変数を受け取った方のmoduleではカンマ区切りの変数をsplit関数でlistに変換し、index関数で目的のkeyを参照、element関数でvalueを最終的に取得しています。この関数を利用することで、一つのmapping定義だけで済むという利点があります。ちなみにですがjoin関数を利用しないとどうやら「B780FFEC...」という文字列に変換されてしまうようなので、指定する必要があります。
実行してみましょう。
$ terraform output us-east-1_image_id = image-1234 us-west-2_image_id = image-4567
ちゃんと値を取得できているようですね。今回の例では terraform.tfvars
を利用しましたが、 *.tf
に直接mapping定義する場合でも同じく出力してくれます。
テスト6: module側でmappingを再作成する
テスト5の例を見た方の中には「module側でmappingを再作成すると良いのでは」と思った方もいらっしゃるのではないでしょうか。テスト5の例は、確かに一つのmappingだけを定義すれば良いという利点がありますが、module側では単なる文字列を渡しているだけです。そのためさまざまな関数を利用して文字列処理を実施しています。
それならいっそmoduleに渡した変数からmodule側でmappingを再作成すれば、妙な文字列処理せず素直なコードになるのではないでしょうか。試してみまょう。
terraform.tfvars
images.us-east-1 = "image-1234" images.us-west-2 = "image-4567"
main.tf
variable "images" { default = {} } module "test" { source = "./test" image_keys = "${join(",", keys(var.images))}" image_values = "${join(",", values(var.images))}" } output "us-east-1_image_id_via_lookup" { value = "${module.test.us-east-1_image_id_via_lookup}" } output "us-east-1_image_id_via_dot" { value = "${module.test.us-east-1_image_id_via_dot}" } output "us-west-2_image_id_via_dot" { value = "${module.test.us-west-2_image_id_via_dot}" } output "us-west-2_image_id_via_lookup" { value = "${module.test.us-west-2_image_id_via_lookup}" }
test/test.tf
variable "image_keys" {} variable "image_values" {} variable "images" { default = { us-east-1 = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-east-1"))}" us-west-2 = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-west-2"))}" } } output "us-east-1_image_id_via_lookup" { value = "${lookup(var.images, "us-east-1")}" } output "us-east-1_image_id_via_dot" { value = "${var.images.us-east-1}" } output "us-west-2_image_id_via_lookup" { value = "${lookup(var.images, "us-west-2")}" } output "us-west-2_image_id_via_dot" { value = "${var.images.us-west-2}" }
変数定義の中で文字列処理しているのであまり意味ないのではという気もしなく無いですが、実行してみましょう。
$ terraform plan There are warnings and/or errors related to your configuration. Please fix these before continuing. Errors: * 1 error(s) occurred: * module test.root: 1 error(s) occurred: * Variable 'images': cannot contain interpolations
残念ながら現在のところTerraformは変数定義の中での変数展開をサポートしていません。一応null_resourceやtemplate_fileを利用したワークアラウンドがあるようです。
v0.7.0では対応されているのか
では肝心のv0.7.0ではmappingをmoduleへ渡す機能に対応しているのでしょうか。「a47ad103e2320a40071a54400be46cf24071091b(v7.0.0-dev)」をコンパイルし、確認してみましょう。コンパイルの方法はREADMEを参照してください。コンパイルが完了すると $GOPATH/bin
ディレクトリ以下にTerraformのバイナリが出力されます。
$ $GOPATH/bin/terraform version Terraform v0.7.0-dev (a47ad103e2320a40071a54400be46cf24071091b)
結果
テスト3-4実行してみたのですがうまく動作してないようでした。CHANGELOGを見るとjsonencode関数がそれらしい記述をしているのですが、これのことなのか。v0.7.0がでたら追記したいと思います。
追記
2016/08/02にTerraform v0.7.0がリリースされました。v0.7.0へのアップグレードガイドの「Migrating to native lists and maps」に記述されていますが、Moduleに対してlist及びmapを渡すことが可能になりました!
まとめ
いかがだったでしょうか。
はやくv0.7.0リリースしてくれ!!!111。
本エントリがみなさんの参考になれば幸いです。